package src.ee.ioc.smaug.fsm;

import java.util.ArrayList;

/**
 * Class for describing state machines
 * 
 * @author Margarita Spichakova
 * 
 */
public abstract class FiniteStateMachine {

    /**
     * Input alphabet
     */
    Alphabet inputAlphabet;

    /**
     * Output alphabet
     */
    Alphabet outputAlphabet;

    /**
     * Set of states
     */
    ArrayList<State> states;

    /**
     * Set of transitions
     */
    ArrayList<Transition> transitions;

    /**
     * Transition table
     */
    int[][] transitionTable;

    /**
     * Current state of the machine
     */
    State currentState;

    /**
     * String given to input of the FSM
     */
    String inString = "";

    /**
     * String we've got as a result of run
     */
    private String outString = "";

    /**
     * Set of reachable states
     */
    ArrayList<State> reachableStates = new ArrayList<State>();

    /**
     * @return number of states in fsm
     */
    public int getNumOfStates() {
        int num = 0;
        for (int i = 0; i < states.size(); i++) {
            if (states.get(i).isActive()) {
                num++;
            }
        }
        return num;
    }

    /**
     * FSM constructor
     */
    public FiniteStateMachine() {
        inputAlphabet = new Alphabet();
        outputAlphabet = new Alphabet();

        states = new ArrayList<State>();

        transitions = new ArrayList<Transition>();
    }

    /**
     * Set sizes for transition tables
     */
    public void setBounds() {
        transitionTable = new int[states.size()][inputAlphabet
                .getSizeOfAlphabet()];
        for (int i = 0; i < states.size(); i++) {
            for (int j = 0; j < inputAlphabet.getSizeOfAlphabet(); j++) {
                transitionTable[i][j] = -1;
            }
        }
    }

    /**
     * Add state to the fsm
     * 
     * @param stateToAdd
     */
    public void addState(State stateToAdd) {
        states.add(stateToAdd);

    }

    /**
     * Add transition to fsm
     * 
     * @param trans
     */
    public void addTransition(Transition trans) {
        transitions.add(trans);

        transitionTable[trans.getFromState().getStateID()][inputAlphabet
                .findPosition(trans.getInputSymbol())] = transitions
                .indexOf(trans);
    }

    /**
     * Return transition for given position
     * 
     * @param position
     *            given number of the position
     * @return transition
     */
    public Transition getTransition(int position) {
        if (position != -1) {
            return transitions.get(position);
        } else {
            return null;
        }
    }

    /**
     * Return the number of transitions
     * 
     * @return size of transitions array
     */
    public int getNumOfTransitions() {
        return transitions.size();
    }

    /**
     * Make transition
     * 
     * @param inputChar
     *            reading this symbol
     */
    abstract public void makeTransition(char inputChar);

    /**
     * Run machine
     */
    abstract public void runMachine();

    /**
     * Remove unreachable states
     */
    public void removeUnreachableStates() {
        int nextToCheckIndex = 0;
        reachableStates.add(states.get(0));
        addStatesToReachable(reachableStates.get(nextToCheckIndex).getStateID());
        nextToCheckIndex++;

        while (nextToCheckIndex < reachableStates.size()) {
            addStatesToReachable(reachableStates.get(nextToCheckIndex)
                    .getStateID());
            nextToCheckIndex++;
        }

        for (int i = 0; i < reachableStates.size(); i++) {
            reachableStates.get(i).setReachable(true);
        }

    }

    /**
     * If state reachable then each state which is reachable from given state
     * must be added to reacheble states
     * 
     * @param check
     *            given state
     */
    private void addStatesToReachable(int check) {
        for (int j = 0; j < inputAlphabet.getSizeOfAlphabet(); j++) {
            Transition tr = getTransition(transitionTable[check][j]);
            if (!reachableStates.contains(tr.getToState())) {
                reachableStates.add(tr.getToState());
            }
        }
    }

    /**
     * @param position
     *            of the state
     * @return state at position
     */
    public State getStateAtPosition(int position) {
        return states.get(position);
    }

    /**
     * Returns input string
     * 
     * @return input string
     */
    public String getInString() {
        return inString;
    }

    /**
     * Sets input string
     * 
     * @param inString
     *            given string
     */
    public void setInString(String inString) {
        this.inString = inString;
    }

    /**
     * Returns input alphabet
     * 
     * @return alphabet
     */
    public Alphabet getInputAlphabet() {
        return inputAlphabet;
    }

    /**
     * Sets input alphabet
     * 
     * @param inputAlphabet
     */
    public void setInputAlphabet(Alphabet inputAlphabet) {
        this.inputAlphabet = inputAlphabet;
    }

    /**
     * Returns out alphabet
     * 
     * @return alphabet
     */
    public Alphabet getOutputAlphabet() {
        return outputAlphabet;
    }

    /**
     * Sets out alphabet
     * 
     * @param outputAlphabet
     */
    public void setOutputAlphabet(Alphabet outputAlphabet) {
        this.outputAlphabet = outputAlphabet;
    }

    /**
     * return state at position
     * 
     * @param position
     *            given position of the state
     * @return state at position
     */
    public State getState(int position) {
        return states.get(position);
    }

    /**
     * The result of run
     * 
     * @return result string
     */
    public String getOutString() {
        return outString;
    }

    /**
     * Set the result of run
     * 
     * @param outString
     *            given string
     */
    public void setOutString(String outString) {
        this.outString = outString;
    }

    /**
     * Is machine still running
     */
    private boolean running = true;

    /**
     * @return true if machine running
     */
    public boolean isRunning() {
        return running;
    }

    /**
     * Set machine running or not
     * 
     * @param running
     */
    public void setRunning(boolean running) {
        this.running = running;
    }
}
